package be.rivendale.mathematics.intersection;

import be.rivendale.mathematics.*;
import org.apache.commons.lang.Validate;

import static be.rivendale.mathematics.MathematicalUtilities.between;

/**
 * Represents the intersection between a {@link be.rivendale.mathematics.Ray} and a {@link be.rivendale.mathematics.Triangle}.
 * More specifically it represents the results of an intersection test between the two. This can or can not be
 * an intersection, depending on the properties of this object (which can be queried of course).
 * <p><strong>Note:</strong> The mere existance if this object does not mean an intersection exists.
 * <p>The algorythm to determine the intersection is an implementation of the Mller-Trumbore Ray-Triangle intersection test</p>
 * <p>An intersection means that the following conditions are met:
 * <ul><li>The ray and the triangle cross in a point that is within the triangle</li><li>The ray and the triangle cross
 * in a point on the ray that is not between the origin and the secondary ray points (!)</li>
 * <li>The ray and the triangle are <strong>not</strong> coplanar (so when they overlap with eachother, this is treated as not intersecting)
 * This is an acceptable simplification because in this case they would share an infinite number of points,
 * which is difficult to model. Also, the intersection would not be visible anyway because a triangle does not have a "thickness".</li></ul></p>
 * You need to query the {@link #doesIntersect()} (for example) method to check if an intersection actually exists.</p>
 */
public class Intersection {
	private double u;
	private double v;
	private Point intersectionPoint;

	/**
	 * Creates an intersection between a ray, and an object that lies on a plane.
	 * @param ray The ray that is to be tested for intersection with a geometry object.
	 * @param a A point of the polygon to test for intersection.
	 * @param b A point of the polygon to test for intersection.
	 * @param c A point of the polygon to test for intersection.
	 * @param rayIntersectionMode The intersection mode regarging to the ray.
	 * @param polygonIntersectionMode The intersection mode rgarding to the polygon.
	 */
	private void calculateIntersection(Ray ray, Point a, Point b, Point c, RayIntersectionMode rayIntersectionMode, PolygonIntersectionMode polygonIntersectionMode) {
		Validate.notNull(ray, "An intersection between a ray and a triangle can only be calculated if the ray is not null");
		Validate.notNull(rayIntersectionMode,  "An intersection mode is required to calculate an intersection point.");

        Vector vectorBetweenTrianglePointAndRayOrigin = Triple.vectorBetweenPoints(a, ray.getOrigin());
        Vector firstTriangleEdgeVector = Triple.vectorBetweenPoints(a, b);
        Vector secondTriangleEdgeVector = Triple.vectorBetweenPoints(a, c);
        Vector rayDirection = ray.direction();
        Vector vectorPerpendicularToRayAndTriangleEdge = rayDirection.crossProduct(secondTriangleEdgeVector);
        double denominator = vectorPerpendicularToRayAndTriangleEdge.dotProduct(firstTriangleEdgeVector);

        // The ray and the triangle are parallel, and thus do not intersect.
        this.u = vectorPerpendicularToRayAndTriangleEdge.dotProduct(vectorBetweenTrianglePointAndRayOrigin) / denominator;
        if(!between(0, 1, this.u)) {
            return;
        }

        Vector Q = vectorBetweenTrianglePointAndRayOrigin.crossProduct(firstTriangleEdgeVector);

        // The ray and the triangle are not parallel, but they miss eachother.
        this.v = Q.dotProduct(rayDirection) / denominator;
        if(!between(0, 1, this.v) || !polygonIntersectionMode.isValidIntersectionPoint(u, v)){
            return;
        }

        // If the intersection does occur, but is somewhere behind the ray's origin, or between the ray and the secondary point.
        // We classify this as a miss, because when casting this as a primary ray the intersection would be behind
        // the view pane.
        double t = Q.dotProduct(secondTriangleEdgeVector) / denominator;
        if(!rayIntersectionMode.isValidIntersectionPoint(t)) {
            return;
        }

        intersectionPoint = ray.pointOnRay(t);
	}

	/**
     * Creates an intersection between the specified ray and triangle.
     * <p>This method performs some precalculations which can be reused in further processing.
     * Therefor you can expect this constructor to be a relative expensive operation.</p>
     * @param ray The ray to test for an intersection with the triangle.
     * @param triangle The triangle to test for an intersection with the ray.
     * @param rayIntersectionMode The ray intersection mode to evaluate the intersection with.
     */
    public Intersection(Ray ray, Triangle triangle, RayIntersectionMode rayIntersectionMode) {
		Validate.notNull(triangle, "To calculate the intersection between a ray and a triangle, the triangle must not be null");
		calculateIntersection(ray, triangle.getA(), triangle.getB(), triangle.getC(), rayIntersectionMode, PolygonIntersectionMode.triangle);
    }

	/**
	 * Creates an intersection between the specified ray and rectangle.
	 * <p>This method performs some precalculations which can be reused in further processing. Therefor you can expect
	 * this constructor to be a relative expensive operation.</p>
	 * @param ray The ray to test for an intersection with.
	 * @param rectangle The rectangle to test for an intersection with.
	 * @param rayIntersectionMode The intersection mode regarding to the ray for this intersection test.
	 */
	public Intersection(Ray ray, Rectangle rectangle, RayIntersectionMode rayIntersectionMode) {
		Validate.notNull(rectangle, "To calculate the intersection between a ray and a rectangle, the rectangle must not be null");
		calculateIntersection(ray, rectangle.getA(), rectangle.getB(), rectangle.getC(), rayIntersectionMode, PolygonIntersectionMode.parallelogram);
	}

    /**
     * Returns the intersection point of the ray and the triangle, or returns null if such an intersection point
     * does not exist. In the latter case a call to {@link #doesIntersect()} would return false as well.
     * @return The point at which the ray and the triangle intersect. May be null if there is no intersection.
     */
    public Point getIntersectionPoint() {
        return intersectionPoint;
    }

    /**
     * Checks if the ray and the triangle intersect.
     * <p>This method is for the simplest case where the client only needs to know if the ray and the triangle intersect
     * or not, not at which point exactly.</p>
     * @return True if the ray and the triangle interesect with eachother, false otherwise.
     */
    public boolean doesIntersect() {
        return intersectionPoint != null;
    }
}
